/* 
 * StikiWeb Wiki - a Wiki for Hierarchical Organizations
 * $Id: PageInfoNode.java,v 1.2 2004/05/07 14:57:17 biglee Exp $
 * 
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the StikiWeb Wiki.
 *
 * The Initial Developer of the Original Code is Lee Haslup (biglee@haslups.com).
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * ***** END LICENSE BLOCK ***** 
 */
package org.stikiweb.contentMagnager;

import java.io.File;
import java.io.FilenameFilter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.Vector;

import org.stikiweb.translations.StikiWebTranslator;

/**
 * @author biglee
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public class PageInfoNode {
	public static HashMap pageMap;
	
	protected String name;
	protected long size;
	protected long attSize;
	protected long lastReportedSize = -1;
	protected long lastAvailable = -1;
	protected PageInfoNode parent;
	protected TreeMap children = new TreeMap();
	protected TreeMap attachments = new TreeMap();
	
	// ==========
	// ACCESSORS
	// ==========
	/**
	 * @return Returns the name.
	 */
	public String getName() {
		return name;
	}
	/**
	 * @return Returns the parent node.
	 */
	public PageInfoNode getParent() {
		return parent;
	}
	/**
	 * @return Returns the children.
	 * 
	 * Note: this vector is a shared object.
	 * Caller should not make changes to the Vector
	 * or hold on to it or its contents longer than 
	 * a single message send.
	 */
	public TreeMap getChildren() {
		return children;
	}
	/**
	 * @return Returns the attachments.
	 * 
	 * Note: Shared object.
	 * Caller is requested not to make changes.
	 */
	public TreeMap getAttachments() {
		return attachments;
	}
	/**
	 * @return Returns the size of the page.
	 */
	public long getSize() {
		return size;
	}
	/**
	 * @return Returns the sum of the size of all attachments.
	 */
	public long getAttSize() {
		return attSize;
	}
	/**
	 * @param attachmentName The attachment to add.
	 */
	public synchronized void addAttachment(String attachmentName) {
		if(attachmentName==null) {
			return;
		}
		// make sure we don't add duplicate attachments (as when user
		// replaces an existing att with a new version). 
		Iterator it = attachments.values().iterator();
		boolean dup = false;
		while(it.hasNext() && !dup) {
			String existingAtt = (String)it.next();
			if(attachmentName.equals(existingAtt)) {
				dup=true;
			}
		}
		if(!dup) {
			this.attachments.put(attachmentName,attachmentName);
		}
		recalcAttSize();
	}
	
	/**
	 * @param attachmentName The attachments to drop.
	 */
	public synchronized void dropAttachment(String attachmentName) {
		this.attachments.remove(attachmentName);
		recalcAttSize();
	}
	/**
	 * @param child - The child to add.
	 */
	public synchronized void addChild(PageInfoNode child) {
		this.children.put(child.name, child);
		child.parent=this;
		pageMap.put(child.name,child);
		child.markForRecalc();
	}
	/**
	 * @param child - The child to drop.
	 */
	public synchronized void dropChild(PageInfoNode child) {
		this.children.remove(child.name);
		child.parent=null;
		pageMap.remove(child.name);
		markForRecalc();
	}
	
	/**
	 * recalcAttSize should be called whenever the list of attachments is
	 * changed (or initialized) to make sure that the sum of the sizes is
	 * maintained.  
	 * 
	 * Note: the current behavoir gives the sum of the actual sizes and not
	 * the sum of the sizes on disk.  This will underestimate the size by
	 * up to the disk block size for each file.
	 * 
	 */
	// TODO fix this to block up the sizes to get a more accurate total.
	protected synchronized void recalcAttSize() {
		Iterator it = attachments.values().iterator();
		long newSize = 0;
		while(it.hasNext()) {
			String attName = (String)it.next();
			newSize += lookUpAttSize(name,attName);
		}
		attSize = newSize;
		markForRecalc();
	}
	
	/**
	 * Look up the size of an attachment.
	 * @param parent
	 * @param attName
	 * @return
	 */
	public static long lookUpAttSize(String parent, String attName) {
		String wikiBase = StikiWebContext.solo().wikiBase;
		String fullAttName = parent + "/" + attName;
		String fullFileName = wikiBase + StikiWebTranslator.convertName(fullAttName);
		File attFile = new File(fullFileName);
		if(attFile.isFile()) {
			return attFile.length();
		}
		return 0;
	}
	
	/**
	 * setPageSize should be called whenever the page is saved 
	 * (or this structure is initialized) to make sure that the size
	 * of the page is accurate.  
	 * 
	 * Note: the current behavoir gives the sum of the actual sizes and not
	 * the sum of the sizes on disk.  This will underestimate the size by
	 * up to the disk block size for each file.
	 * 
	 */
	// TODO fix this to block up the sizes to get a more accurate total.
	public synchronized void resetPageSize() {
		String wikiBase = StikiWebContext.solo().wikiBase;
		String fullFileName = wikiBase + StikiWebTranslator.convertName(name);
		File theFile = new File(fullFileName);
		if(theFile.isFile()) {
			size = theFile.length();
		}
		markForRecalc();
	}
	
	public static synchronized long availableStorage(String name) {
		
		// find the node for this page.
		PageInfoNode theNode = forName(name);
		
		// not there?  return zero (for lack of a better option).
		if(theNode==null) {
			return 0;
		}
		
		// if we already know the available storage, return the previous value.
		if(theNode.lastAvailable>=0) {
			return theNode.lastAvailable;
		}
		
		// if this page has a page spec that defines a maximum storage limit...
		PermissionManager pm = PermissionManager.solo();
		PermissionManager.PageSpec spec = pm.getPageSpec(("..".equals(name))?"":name);
		if(spec!=null && spec.maxStorage>0) {
			// then the available storage is at most the local limit minus the amount used locally
			long localAvail = spec.maxStorage*100000 - theNode.size - theNode.attSize;
			Iterator it = theNode.children.values().iterator();
			while(it.hasNext()) {
				localAvail -= reportedSize(((PageInfoNode)it.next()).name);
			}
			// if the storage is reserved then the local limit all we need to worry about.
			// (Storage at the top level is always reserved so check parent=null)
			if(spec.reservedStorage || theNode.parent == null) {
				theNode.lastAvailable = localAvail;
				return localAvail;
			}
			// otherwise (storage not reserved) we need to see if the parent's available is 
			// more restrictive.
			long parentsAvail = availableStorage(theNode.parent.name);
			
			// use the most restrictive value.
			theNode.lastAvailable = (parentsAvail < localAvail) ? parentsAvail : localAvail;
			return theNode.lastAvailable;
		}
		
		// This page doesn't have a spec that tells its maximum storage.  If there is a parent
		// ask the parent for the available storage.
		if(theNode.parent!=null) {
			theNode.lastAvailable = availableStorage(theNode.parent.name);
			return theNode.lastAvailable;
		}
		
		// Can't find a limit anywhere.  Just return 20 meg.  Note that this is effectively
		// unlimited since it doesn't go down as storage is used.  It will limit the amount 
		// for any one upload to 20 meg.
		theNode.lastAvailable = 20000000;
		return theNode.lastAvailable;
	}
	
	/**
	 * This method is called whenever any change is made to a pages storage totals.  It
	 * notifies all parent nodes that they must recalculate their reported size and 
	 * available storage (before they are used.)
	 * @param name
	 * @return
	 */
	protected void markForRecalc() {
		lastReportedSize = -1;
		lastAvailable = -1;
		if(parent!=null) {
			parent.markForRecalc();
		}
	}
	
	/**
	 * This is called whenever the Admin.Tables file is 
	 * updated.  It marks all nodes in the tree as needing
	 * to recalculate their lastReportedSize and lastAvailable
	 * values.
	 *
	 */
	public static void markAllForRecalc() {
		forName("..").markTreeForRecalc();
	}
	
	/**
	 * recursive, non-static method that does the work for
	 * markAllForRecalc.
	 *
	 */
	protected void markTreeForRecalc() {
		lastReportedSize = -1;
		lastAvailable = -1;
		Iterator it = children.values().iterator();
		while(it.hasNext()) {
			((PageInfoNode)it.next()).markTreeForRecalc();
		}
	}
	
	/**
	 * The reported size of a page is the amount of storage (in bytes) that it (and its
	 * children and attachments) have either used or reserved.  
	 * @param name
	 * @return
	 */
	public static synchronized long reportedSize(String name) {
		
		// find the node for this page.
		PageInfoNode theNode = forName(name);
		
		// not there? return zero.
		if(theNode==null) {
			return 0;
		}
		
		// if we already know the reported size, use the previous value
		if(theNode.lastReportedSize>=0) {
			return theNode.lastReportedSize;
		}
		
		// if this page has a page spec that specifies a reserved
		// value -- report the maximum  regardless of how much we
		// have actually used.
		PermissionManager pm = PermissionManager.solo();
		PermissionManager.PageSpec spec = pm.getPageSpec(("..".equals(name))?"":name);
		if(spec!=null && spec.reservedStorage) {
			theNode.lastReportedSize = spec.maxStorage*100000;
			return theNode.lastReportedSize;
		}
		
		long retval = actualSize(name);
		theNode.lastReportedSize = retval;
		return retval;
	}
	
	/**
	 * The actual space used by this page and its attachments plus the
	 * sum of the reported sizes (space used or reserved) by its children.
	 * @param name
	 * @return
	 */
	public static long actualSize(String name) {
		
		// find the node for this page.
		PageInfoNode theNode = forName(name);
		
		// not there? return zero.
		if(theNode==null) {
			return 0;
		}
		
		// The actual amount of storage is the amount for this
		// page and its attachments...
		long retval = theNode.size + theNode.attSize;
		
		// ... plus the sum of its children's reported sizes.
		Iterator it = theNode.children.values().iterator();
		while(it.hasNext()) {
			retval += reportedSize(((PageInfoNode)it.next()).name);
		}
		return retval;
		
	}
	
	public synchronized static void addPage(String name) {
		PageInfoNode existingNode = forName(name);
		if(existingNode==null) {
			if(name.endsWith(".wiki")) {
				name = name.substring(0,name.length()-5);
			}
			int loc = name.lastIndexOf(('.'));
			String parent = "..";
			if(loc>0) {
				parent = name.substring(0,loc);
			}
			PageInfoNode parentNode = forName(parent);
			PageInfoNode newChild = new PageInfoNode();
			newChild.name=name;
			pageMap.put(name,newChild);
			parentNode.addChild(newChild);
			newChild.resetPageSize();
		}
		else {
			existingNode.resetPageSize();
		}
	}

	public synchronized static void dropPage(String name) {
		if(forName(name)!=null) {
			if(name.endsWith(".wiki")) {
				name = name.substring(0,name.length()-5);
			}
			int loc = name.lastIndexOf(('.'));
			String parent = "..";
			if(loc>0) {
				parent = name.substring(0,loc);
			}
			PageInfoNode parentNode = forName(parent);
			PageInfoNode thisNode = forName(name);
			parentNode.dropChild(thisNode);
			pageMap.remove(name);
		}
	}
	
	public synchronized static PageInfoNode forName(String name) {
		if(name.endsWith(".wiki")) {
			name = name.substring(0,name.length()-5);
		}
		if(name.length()==0) {
			name = "..";
		}
		return (PageInfoNode) pageMap().get(name);
	}
	
	protected synchronized static HashMap pageMap() {
		if(pageMap==null) {
			buildMap(StikiWebContext.solo().wikiBase);
		}
		return pageMap;
	}
	
	public synchronized static void buildMap(String wikiBase) {
		
		// Create a new (empty) map to hold the result
		pageMap = new HashMap();
		
		// The top level node in the Wiki tree represents "Wiki Base" -- the non-existant
		// page with the empty name.  (The name is often coded as ".." as it is here.)
		PageInfoNode top = new PageInfoNode();
		top.name="..";
		// Wiki Base should never be saved so it should always be size=0
		top.size=0;
		// It can have attachments though .. this may get reset in recursion.
		top.attSize=0;
		// put it in the map with the name ".."
		pageMap.put("..",top);
		
		// Start at the top of the directory tree.
		File topDir = new File(wikiBase);
		
		// ------------------------------------
		// Start the recursive map build
		// -------------------------------------
		addToMap(wikiBase, topDir, top);
		

	}
	
	protected static void addToMap(String curDirPath, File curDir, PageInfoNode parent) {
		
		// -------------------------
		// Handle Attachments
		// -------------------------
		
		// get a list of the files in the directory that don't end with .wiki and are not
		// directories.  These are the page attachments.
		FilenameFilter attFilter = new FilenameFilter()
		  {public boolean accept(File dir, String name)
			{return (!name.endsWith(".wiki")) && !(new File(dir,name)).isDirectory();}
		  };
		String[] atts = curDir.list(attFilter);
		
		//Add attachments to parent's list
		for(int i=0; i<atts.length; i++) {
			parent.getAttachments().put(atts[i],atts[i]);
		}
		
		// look up the sizes of the attachments and set the parents attSize
		parent.recalcAttSize();

		// -------------------------
		// Handle Children 
		// -------------------------
		
		// get a list of the files in the directory that end with .wiki (the wiki pages).
		// these are the child pages of the parent
		FilenameFilter pageFilter = new FilenameFilter()
		  {public boolean accept(File dir, String name)
			{return name.endsWith(".wiki");}
		  };
		String[] wikiPages = curDir.list(pageFilter);
		
		// Prefix for child name because it is child of this parent.
		String parentNamePref = "";
		if(!parent.name.equals("..")) {
			parentNamePref = parent.name + ".";
		}
		
		// For each child page, create a node on the parent's "children" list.
		for(int i=0; i<wikiPages.length; i++) {
			
			PageInfoNode child = new PageInfoNode();
			
			// child page name does not include the .wiki extension
			String childsRelativeName = wikiPages[i].substring(0,wikiPages[i].length()-5);
			
			// child's full name is parent name plus dot plus child's relative name
			child.name=parentNamePref + childsRelativeName;
			
			// put the child in the map
			pageMap.put(child.name,child);
			
			// look up the child's size
			child.resetPageSize();
			
			// start the child at attSize zero.  If it has attachments this will be reset.
			child.attSize=0;
			
			// cretate two-way child-parent link
			child.parent = parent;
			parent.children.put(child.name,child);
			
			// Check to see if child has a directory for children or attachments.
			String childsAttDirName = curDirPath + childsRelativeName +"/";
			File childsAttDir = new File(childsAttDirName);
			if(childsAttDir.exists() && childsAttDir.isDirectory()) {
				
				// Recurse to pick up this child's attachments and children.
				addToMap(childsAttDirName, childsAttDir, child);
			}
		}
		
	}
	
	public static String debugDump() {
		String retval = "\nPageInfoNodes:\n--------------\n\n";
		Vector unreachable = new Vector();
		unreachable.addAll(pageMap.values());
		Vector unmapped = new Vector();
		String name = "..";
		PageInfoNode node = forName(name);
		retval += debugNodeDump(node, unreachable, unmapped, "");
		if(unreachable.size()>0) {
			Iterator it = unreachable.iterator();
			retval += "\nUnreachable:\n-------------\n";
			while(it.hasNext()) {
				retval += ((PageInfoNode)it.next()).name + "\n";
			}
		}
		if(unmapped.size()>0) {
			Iterator it = unmapped.iterator();
			retval += "\nUnmapped:\n---------\n";
			while(it.hasNext()) {
				retval += ((PageInfoNode)it.next()).name + "\n";
			}
		}
		return retval;
	}
	protected static String debugNodeDump(PageInfoNode node, Vector unreachable, Vector unmapped, String prefix) {
		String enoughDots = "....................................................................................";
		int pad = 50 - prefix.length() - node.name.length();
		if(pad < 0) pad=0;
		String dots = enoughDots.substring(0,pad);
		String retval = prefix  + "--" + node.name + dots + "size/attSize = " + node.size + "/" + node.attSize;
		if(forName(node.name)==null) {
			retval += " *** NOT MAPPED ***";
			unmapped.add(node);
		}
		retval += "(" + availableStorage(node.name) + " available)";
		retval += "\n";
		unreachable.remove(node);
		Iterator it = node.attachments.values().iterator();
		while(it.hasNext()) {
			String att = (String)it.next();
			pad = 52 - prefix.length() - att.length();
			dots=enoughDots.substring(0,pad);
			retval += prefix + "  +->" + att + dots +  "size = " + lookUpAttSize(node.name,att) +"\n";
		}
		it = node.children.values().iterator();
		while(it.hasNext()) {
			retval += debugNodeDump((PageInfoNode)it.next(),unreachable,unmapped,prefix + "  |");
		}
		return retval;
	}
}
